阅读指南
上一节学会了通过 API 获取 Embedding 向量——给文字贴上了"语义坐标"。但有了向量之后呢?
假设知识库里有成千上万条文档,每条都变成了高维向量。用户提一个问题,需要从海量向量中找出最相关的几条。一个个比对效率极低。这一节解决的就是这个核心问题。
从一个真实场景说起。
一个现实的难题
假设做一个企业知识库问答系统。先看数据规模:
现在用户问:"年假怎么申请?"
系统需要做什么?
第2步是关键。怎么在10万个向量里快速找到最相似的那5个?
朴素方法的困境
最直接的想法是什么?"暴力搜索"——一个个比对。
# 伪代码:朴素的相似度搜索
question_vec = get_embedding("年假怎么申请?")
similarities = []
for i, doc_vec in enumerate(all_100k_vectors):
# 计算余弦相似度
sim = cosine_similarity(question_vec, doc_vec)
similarities.append((sim, i))
# 排序,取前5个
top_5 = sorted(similarities, reverse=True)[:5]
这段代码有什么问题?
算笔账:
2-3秒什么概念?想想这些场景:
在真实的生产环境中,这个问题会被成倍放大:
这就是为什么需要向量数据库。
向量数据库做了件很聪明的事:它不会每次都傻傻地遍历所有向量,而是提前把向量组织成特殊的数据结构,让检索可以"跳着走"。
打个比方。
朴素搜索 = 在字典里逐页翻查
要找"Apple"这个词
从第1页开始,一页页翻,直到找到
结果:翻了100页才找到
向量数据库 = 用索引和目录
要找"Apple"
- 看首字母索引 → 直接跳到"A"开头的部分
- 看第二个字母 → 跳到"Ap"
- 看第三个字母 → 定位到"App"
- 找到"Apple"
翻页次数:只需3-5次
这不是快一点点,是快到离谱。向量数据库的核心优势就在这:
向量数据库的核心技术叫近似最近邻搜索(Approximate Nearest Neighbor,ANN)。名字听起来有点拗口,但思想很简单。
核心思想:空间分区
最好理解的方法就是"空间分区"——把向量空间划分成多个区域,查询时只需要在最相关的几个区域里搜索。
用生活中的例子来理解。
想象在一个能容纳10万人的体育场里,要随便找到一位穿红色衣服的观众。怎么找?
暴力搜索
从第1排第1座开始,按座位顺序一个个检查每个人的衣服颜色,
直到终于遇到一位穿红衣服的观众。
如果平均检查每个人需要 1 秒,想靠这种方式查一遍,这得看运气,最长需要30个小时左右。
分区搜索(IVF 的思路)
- 先把体育场划分成100个看台区域;
- 快速扫一眼每个区域的大致颜色分布,
找出"红衣服明显更多"的3个区域;- 只在这3个区域里逐排逐座细查。
这样只需要仔细检查几千个人,耗时从"几个小时"降到"几十分钟"。
图搜索(HNSW 的思路)
- 从体育场入口的引导员开始打听;
- 引导员说:"我刚才看到A区红衣服比较多,去A区问问";
- 到了A区后,当地引导员再指"A3看台那一片红衣服最多";
- 走到A3,看台管理员直接指"就在这边第5排";
- 几分钟内,就锁定到了那一小撮红衣服观众中的一位。
整个过程只和少数几个"关键节点"打了交道,而不是亲自看过10万人。
向量数据库就是用类似的思想,把高维空间中的向量组织起来。下面看三种主流的索引算法。了解即可。
核心思想:聚类 + 倒排
第1步:聚类
图示:
向量空间(简化为2D平面):
簇3 簇1
★ ●
★ ★ ★ ● ●
★ ★中心 ★ ● ●中心 ●
★ ★ ● ●
★ ●
簇4 簇2
■ ▲
■ ■ ▲ ▲
■ ■中心 ■ ▲ ▲中心 ▲
■ ■ ▲ ▲
■ ▲
解读:
第2步:查询
用一个示例来理解。假设要找"Python 语言怎么样?"的相关内容:
步骤1:计算查询向量到所有1000个簇中心的距离
簇1(编程语言): 距离 = 0.2 ← 最近
簇2(美食菜谱): 距离 = 0.9
簇3(旅游攻略): 趻离 = 0.85
...
簇15(技术教程): 距离 = 0.18 ← 最近
...步骤2:选择最近的10个簇
簇15(技术教程)、簇1(编程语言)、簇8(软件开发)...
这些簇共有 1000 个向量步骤3:只在这 1000 个向量中搜索
找到:
- “Python 入门教程:基础语法” (距离 = 0.10)
- “Python 与其他语言对比” (距离 = 0.13)
- “Python 应用场景分析” (距离 = 0.16)
结果:返回这 3 条相关内容
核心思想:多层图结构 + 贪婪搜索
HNSW把向量组织成一个多层的图结构。
整体结构
第2层(稀疏):只有少数节点
A --------------------------- D
第1层(中等):节点增多
A ------- B ------- D
| |
C ------- E
第0层(密集):包含所有节点
A - B - D - F
| | | |
H - C - E - G
查询流程
查找示例
假设要找"Python 数据分析"的相关内容
步骤1:第2层(稀疏,只有10个点)
当前在入口点 A
- 计算查询向量“Python 数据分析”到节点的距离:
- 到 A(前端开发): 0.8
- 到 D(后端框架): 0.5
- D 更近 → 跳到 D
步骤2:第1层(中等,有50个点)
当前在点 D
- 计算查询向量到 D 的邻居的距离:
- 到 A(前端开发): 0.8
- 到 B(Web开发): 0.4
- 到 E(Python教程): 0.3
- E 更近 → 跳到 E
步骤3:第0层(密集,所有100个点)
当前在点 E
- 计算查询向量到 E 的邻居的距离:
- 到 C(Java开发): 0.6
- 到 D(后端框架): 0.5
- 到 G(Python数据分析教程): 0.15
- G 更近 → 跳到 G
- 计算查询向量到 G 的邻居的距离:
- 到 E(Python教程): 0.3
- 到 F(Python编程): 0.4
- 到 H(NumPy库): 0.2
- G 自己(0.15)比所有邻居都近 → 停止
结果:找到 G - "Python数据分析教程"
上述流程最终能找到最相似的"邻近"向量,但不只想找1个"最"相近的,而是想找多个相近的(Top-K)向量。 比如如果要找 Top-5 最相似的结果,那按照上述的流程做5遍吗?并不是。真正的方法如下:
Note
向量怎么分层?
上述图的查找方法,是建立在分层的基础上。一堆向量是怎么分层的?
有兴趣可以阅读下。
其实很简单,完全是随机分层的。
HNSW 建索引时,每个向量都会随机分配一个“层级”:
具体例子:
为什么要随机?
这样设计的好处:
| 中文 | English | 音标 | 说明 |
|---|---|---|---|
| 近似近邻 | Approximate Nearest Neighbor (ANN) | /əˈprɑːksɪmət ˈnɪərəst ˈneɪbər/ | 通过索引结构牺牲少量精度换取极快检索速度的算法 |
| 精确近邻 | K-Nearest Neighbor (KNN) | /keɪ ˈnɪərəst ˈneɪbər/ | 遍历全部数据确保返回最精确的Top-K结果 |
| 倒排文件索引 | Inverted File Index (IVF) | /ɪnˈvɜːrtɪd faɪl ˈɪndeks/ | 通过聚类将向量空间分区,只搜索最近簇的索引方法 |
| 分层可导航小世界图 | Hierarchical Navigable Small World (HNSW) | /ˌhaɪəˈrɑːrkɪkl ˈnævɪɡəbl smɔːl wɜːrld/ | 基于多层图结构的近似搜索算法,查询极快 |
| Voronoi 单元 | Voronoi Cell | /vəˈroʊnɔɪ sel/ | IVF聚类后向量空间被划分成的区域,查询时搜索最近单元 |